package com.andon.microcellsimulator.common.mqtt;

import android.app.AlarmManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.IBinder;
import android.util.Log;

import com.andon.microcellsimulator.ZnkConstant;

import org.eclipse.paho.client.mqttv3.IMqttClient;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttClientPersistence;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;

/* 
 * PushService that does all of the work.
 * Most of the logic is borrowed from KeepAliveService.
 * http://code.google.com/p/android-random/source/browse/trunk/TestKeepAlive/src/org/devtcg/demo/keepalive/KeepAliveService.java?r=219
 */
public class MQTTService extends Service
{
	// this is the log tag
	public static final String		TAG = "MQTTService";

	// the IP address, where your MQTT broker is running.
	private static final String		MQTT_HOST = "andon-test.chinacloudapp.cn";
	// the port at which the broker is running. 
	private static int				MQTT_BROKER_PORT_NUM      = 8883;
	// MQTT persistence.
	private static MqttClientPersistence MQTT_PERSISTENCE     = new MemoryPersistence();
	// We don't need to remember any state between the connections, so we use a clean start. 
	private static boolean			MQTT_CLEAN_START          = true;
	// Let's set the internal keep alive for MQTT to 15 mins. I haven't tested this value much. It could probably be increased.
	private static short			MQTT_KEEP_ALIVE           = 30;
	// Set quality of services to 0 (at most once delivery), since we don't want push notifications 
	// arrive more than once. However, this means that some messages might get lost (delivery is not guaranteed)
	private static int				MQTT_QUALITY_OF_SERVICE   = 0;
	// The broker should not retain any messages.
	private static boolean			MQTT_RETAINED_PUBLISH     = false;
		
	// MQTT client ID, which is given the broker. In this example, I also use this for the topic header. 
	// You can use this to run push notifications for multiple apps with one MQTT broker. 
	public static String			MQTT_CLIENT_ID = "ABCDEF1234567890";

	// These are the actions for the service (name are descriptive enough)
	private static final String		ACTION_START = MQTT_CLIENT_ID + ".START";
	private static final String		ACTION_STOP = MQTT_CLIENT_ID + ".STOP";
	private static final String		ACTION_KEEPALIVE = MQTT_CLIENT_ID + ".KEEP_ALIVE";
	private static final String		ACTION_RECONNECT = MQTT_CLIENT_ID + ".RECONNECT";
	
	// Connectivity manager to determining, when the phone loses connection
	private ConnectivityManager		mConnMan;
	// Notification manager to displaying arrived push notifications 
	private NotificationManager		mNotifMan;

	// Whether or not the service has been started.	
	private boolean 				mStarted;

	// This the application level keep-alive interval, that is used by the AlarmManager
	// to keep the connection active, even when the device goes to sleep.
	private static final long		KEEP_ALIVE_INTERVAL = 1000 * 60 * 28;

	// Retry intervals, when the connection is lost.
	private static final long		INITIAL_RETRY_INTERVAL = 1000 * 10;
	private static final long		MAXIMUM_RETRY_INTERVAL = 1000 * 50;

	// Preferences instance 
	private SharedPreferences 		mPrefs;
	// We store in the preferences, whether or not the service has been started
	public static final String		PREF_STARTED = "isStarted";
	public static final String		PREF_CREATED = "isCreated";
	// Crash flag
	public static final String		PREF_CRASH_FLAG = "crash";
	// Connection Receiver
	public static final String		PREF_CONN_RECEIVER_FLAG = "connectionReceiver";
	// We also store the deviceID (target)
	public static final String		PREF_DEVICE_ID = "deviceID";
	public static final String		PREF_PASSWORD = "password";
	public static final String		PREF_SERVER_ADDR = "serverAddr";
	// We store the topicID
	public static final String		PREF_TOPIC_ID = "topicID";
	// We store the last retry interval
	public static final String		PREF_RETRY = "retryInterval";

	// Notification title
	public static String			NOTIF_TITLE = "Tokudu";
	// Notification id
	private static final int		NOTIF_CONNECTED = 0;
		
	// This is the instance of an MQTT connection.
	private MQTTConnection			mConnection;
	private long					mStartTime;

	private static MQTTCallback mMqttCallback = null;

	private static String mMqttTopicID;

	private boolean isConnecting;

	public static void setMqttCallback(MQTTCallback mc) {
		mMqttCallback = mc;
	}

	// Static method to start the service
	public static void actionStart(Context ctx) {
		Intent i = new Intent(ctx, MQTTService.class);
		i.setAction(ACTION_START);
		ctx.startService(i);
	}

	// Static method to stop the service
	public static void actionStop(Context ctx) {
		Intent i = new Intent(ctx, MQTTService.class);
		i.setAction(ACTION_STOP);
		ctx.startService(i);
	}
	
	// Static method to send a keep alive message
	public static void actionPing(Context ctx) {
		Intent i = new Intent(ctx, MQTTService.class);
		i.setAction(ACTION_KEEPALIVE);
		ctx.startService(i);
	}

	@Override
	public void onCreate() {
		super.onCreate();
		
		log("Creating service");
		mStartTime = System.currentTimeMillis();

		// Get instances of preferences, connectivity manager and notification manager
		mPrefs = getSharedPreferences(TAG, MODE_PRIVATE);
		mConnMan = (ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE);
		mNotifMan = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);

		setCreated(true);
		isConnecting = false;

		setRegisterReceiverFlag(false);

		/* If our process was reaped by the system for any reason we need
		 * to restore our state with merely a call to onCreate.  We record
		 * the last "started" value and restore it here if necessary. */
		handleCrashedService();
	}
	
	// This method does any necessary clean-up need in case the server has been destroyed by the system
	// and then restarted
	private void handleCrashedService() {
		if (wasStarted()) {
			log("Handling crashed service...");
			setCrash(true);

			 // stop the keep alives
			stopKeepAlives();

			// Do a clean start
//			start();
		}
	}
	
	@Override
	public void onDestroy() {
		log("Service destroyed (started=" + mStarted + ")");

		// Stop the services, if it has been started
		if (mStarted) {
			stop();
		}
	}
	
	@Override
	public void onStart(Intent intent, int startId) {
		super.onStart(intent, startId);
		log("Service started with intent=" + intent);

		if (intent != null) {
			// Do an appropriate action based on the intent.
			if (intent.getAction().equals(ACTION_STOP)) {

				stop();

				if (!isCrash()) {
					setCreated(false);
					stopSelf();
				} else {
					setCrash(false);
				}
			} else if (intent.getAction().equals(ACTION_START)) {
				start();
			} else if (intent.getAction().equals(ACTION_KEEPALIVE)) {
				keepAlive();
			} else if (intent.getAction().equals(ACTION_RECONNECT)) {
				if (isNetworkAvailable()) {
					reconnectIfNecessary();
				}
			}

		}
	}
	
	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}

	// log helper function
	private void log(String message) {
		log(message, null);
	}
	private void log(String message, Throwable e) {
		if (e != null) {
			Log.e(TAG, message, e);
			
		} else {
			Log.i(TAG, message);
		}

	}
	
	// Reads whether or not the service has been started from the preferences
	private boolean wasStarted() {
		return mPrefs.getBoolean(PREF_STARTED, false);
	}

	// Sets whether or not the services has been started in the preferences.
	private void setStarted(boolean started) {
		mPrefs.edit().putBoolean(PREF_STARTED, started).commit();
		mStarted = started;
	}

	private void setCreated(boolean started) {
		mPrefs.edit().putBoolean(PREF_CREATED, started).commit();
	}

	// Read crash flag from the preferences
	private boolean isCrash() {
		return mPrefs.getBoolean(PREF_CRASH_FLAG, false);
	}

	// Set crash flag from the preferences
	private void setCrash(boolean crashFlag) {
		mPrefs.edit().putBoolean(PREF_CRASH_FLAG, crashFlag).commit();
	}

	private boolean isRegisterReceiver() {
		return mPrefs.getBoolean(PREF_CONN_RECEIVER_FLAG, false);
	}

	private void setRegisterReceiverFlag(boolean flag) {
		mPrefs.edit().putBoolean(PREF_CONN_RECEIVER_FLAG, flag).commit();
	}

	private void start() {
		log("Starting service...");
		
		// Do nothing, if the service is already running.
		if (mStarted) {
			Log.w(TAG, "Attempt to start connection that is already active");
			return;
		}

		setStarted(true);

		// Establish an MQTT connection
		new MQTTConnectionThread().start();

		if (!isRegisterReceiver()) {
			// Register a connectivity listener
			registerReceiver(mConnectivityChanged, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
			setRegisterReceiverFlag(true);
		}

	}

	class MQTTConnectionThread extends Thread {

		@Override
		public void run() {
			if (!isConnecting) {
				connect();
			}
		}
	}

	private synchronized void stop() {

		if (isRegisterReceiver()) {
			// Remove the connectivity receiver
			unregisterReceiver(mConnectivityChanged);
			setRegisterReceiverFlag(false);
		}

		// Do nothing, if the service is not running.
		if (!mStarted) {
			Log.w(TAG, "Attempt to stop connection not active.");
			return;
		}

		// Save stopped state in the preferences
		setStarted(false);

		// Any existing reconnect timers should be removed, since we explicitly stopping the service.
		cancelReconnect();

		// Destroy the MQTT connection if there is one
		if (mConnection != null) {
			mConnection.disconnect();
			mConnection = null;
		}
	}
	
	// 
	private void connect() {
		log("Connecting...");
		isConnecting = true;
		// fetch the TopicID from the preferences.
		String topicID = mPrefs.getString(PREF_TOPIC_ID, null);
		String mqttServerAddr = mPrefs.getString(PREF_SERVER_ADDR, null);
		// Create a new connection only if the device id is not NULL
		if (mqttServerAddr == null) {
			log("server address not found.");
		} else if (topicID == null) {
			log("topic ID not found.");
		} else {
			try {
				mConnection = new MQTTConnection(mqttServerAddr, topicID.split(","));

			} catch (MqttException e) {
//				setStarted(false);
				// Schedule a reconnect, if we failed to connect
				log("MqttException: " + e);
//	        	if (isNetworkAvailable()) {
//	        		scheduleReconnect(mStartTime);
//	        	}
			}
		}

		isConnecting = false;
	}

	private synchronized void keepAlive() {
		try {
			// Send a keep alive, if there is a connection.
			if (mStarted && mConnection != null) {
				mConnection.sendKeepAlive();
			}
		} catch (MqttException e) {
			log("MqttException: " + (e.getMessage() != null? e.getMessage(): "NULL"), e);
			
			mConnection.disconnect();
			mConnection = null;
			cancelReconnect();
		}
	}

	// Schedule application level keep-alives using the AlarmManager
	private void startKeepAlives() {
		Intent i = new Intent();
		i.setClass(this, MQTTService.class);
		i.setAction(ACTION_KEEPALIVE);
		PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
		AlarmManager alarmMgr = (AlarmManager)getSystemService(ALARM_SERVICE);
		alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP,
		  System.currentTimeMillis() + KEEP_ALIVE_INTERVAL,
		  KEEP_ALIVE_INTERVAL, pi);
	}

	// Remove all scheduled keep alives
	private void stopKeepAlives() {
		Intent i = new Intent();
		i.setClass(this, MQTTService.class);
		i.setAction(ACTION_KEEPALIVE);
		PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
		AlarmManager alarmMgr = (AlarmManager)getSystemService(ALARM_SERVICE);
		alarmMgr.cancel(pi);
	}

	// We schedule a reconnect based on the starttime of the service
	public void scheduleReconnect(long startTime) {
		// the last keep-alive interval
		long interval = mPrefs.getLong(PREF_RETRY, INITIAL_RETRY_INTERVAL);

		// Calculate the elapsed time since the start
		long now = System.currentTimeMillis();
		long elapsed = now - startTime;


		// Set an appropriate interval based on the elapsed time since start 
		if (elapsed < interval) {
			interval = Math.min(interval * 4, MAXIMUM_RETRY_INTERVAL);
		} else {
			interval = INITIAL_RETRY_INTERVAL;
		}
		
		log("Rescheduling connection in " + interval + "ms.");

		// Save the new internval
		mPrefs.edit().putLong(PREF_RETRY, interval).commit();

		// Schedule a reconnect using the alarm manager.
		Intent i = new Intent();
		i.setClass(this, MQTTService.class);
		i.setAction(ACTION_RECONNECT);
		PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
		AlarmManager alarmMgr = (AlarmManager)getSystemService(ALARM_SERVICE);
		alarmMgr.set(AlarmManager.RTC_WAKEUP, now + interval, pi);
	}
	
	// Remove the scheduled reconnect
	public void cancelReconnect() {
		Intent i = new Intent();
		i.setClass(this, MQTTService.class);
		i.setAction(ACTION_RECONNECT);
		PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
		AlarmManager alarmMgr = (AlarmManager)getSystemService(ALARM_SERVICE);
		alarmMgr.cancel(pi);
	}
	
	private void reconnectIfNecessary() {
		if (mStarted && mConnection == null && !isConnecting) {
			log("Reconnecting...");
			new MQTTConnectionThread().start();
		}
	}

	// This receiver listeners for network changes and updates the MQTT connection
	// accordingly
	private BroadcastReceiver mConnectivityChanged = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			// Get network info
			NetworkInfo info = (NetworkInfo)intent.getParcelableExtra (ConnectivityManager.EXTRA_NETWORK_INFO);
			
			// Is there connectivity?
			boolean hasConnectivity = (info != null && info.isConnected()) ? true : false;

			log("Connectivity changed: connected=" + hasConnectivity);

			if (hasConnectivity) {
				reconnectIfNecessary();
			} else if (mConnection != null) {
				// if there no connectivity, make sure MQTT connection is destroyed
				mConnection.disconnect();
				cancelReconnect();
				mConnection = null;
			}
		}
	};
	
	// Display the topbar notification
//	private void showNotification(String text) {
//		Notification n = new Notification();
//
//		n.flags |= Notification.FLAG_SHOW_LIGHTS;
//      	n.flags |= Notification.FLAG_AUTO_CANCEL;
//
//        n.defaults = Notification.DEFAULT_ALL;
//
//		n.icon = R.drawable.icon;
//		n.when = System.currentTimeMillis();
//
//		// Simply open the parent activity
//		PendingIntent pi = PendingIntent.getActivity(this, 0,
//		  new Intent(this, MQTTService.class), 0);
//
//		// Change the name of the notification here
//		n.setLatestEventInfo(this, NOTIF_TITLE, text, pi);
//
//		mNotifMan.notify(NOTIF_CONNECTED, n);
//	}
	
	// Check if we are online
	private boolean isNetworkAvailable() {
		NetworkInfo info = mConnMan.getActiveNetworkInfo();
		if (info == null) {
			return false;
		}
		return info.isConnected();
	}
	
	// This inner class is a wrapper on top of MQTT client.
	private class MQTTConnection implements MqttCallback {
		IMqttClient mqttClient = null;
		
		// Creates a new connection given the broker address and initial topic
		public MQTTConnection(String brokerHostName, String[] initTopic) throws MqttException {
			// Create connection spec
			String mqttConnSpec = "ssl://" + brokerHostName;

			Log.i(TAG, "mqttConnSpec = " + mqttConnSpec);

			// ClientID + DeviceID
			String clientID = MQTT_CLIENT_ID + mPrefs.getString(PREF_DEVICE_ID, "");

			Log.i(TAG, "clientID = " + clientID);

			// Create the client and connect
			mqttClient = new MqttClient(mqttConnSpec, clientID, MQTT_PERSISTENCE);

			// register this client app has being able to receive messages
//			mqttClient.registerSimpleHandler(this);
			mqttClient.setCallback(this);

			SSLContext sslContext = null;
			try {
				sslContext = SSLContext.getInstance("TLS");
				try {
					sslContext.init(null, null, null);
				} catch (KeyManagementException e) {
					e.printStackTrace();
				}
			} catch (NoSuchAlgorithmException e) {
				e.printStackTrace();
			}

			MqttConnectOptions options = new MqttConnectOptions();
			options.setKeepAliveInterval(MQTT_KEEP_ALIVE);
			options.setCleanSession(true);
			options.setConnectionTimeout(10);
			options.setUserName(mPrefs.getString(PREF_DEVICE_ID, ""));
			options.setPassword(mPrefs.getString(PREF_PASSWORD, "").toCharArray());
			Log.i(TAG, "user = [" + mPrefs.getString(PREF_DEVICE_ID, "") + "]");
			Log.i(TAG, "password = [" + mPrefs.getString(PREF_PASSWORD, "") + "]");
			if (sslContext != null) {
				options.setSocketFactory(sslContext.getSocketFactory());
			} else {
				Log.w(TAG, "SSL Context is null!");
			}

			mqttClient.connect(options);

			Log.i(TAG, "has connected to " + mqttConnSpec);
			// Subscribe to an initial topic, which is combination of client ID and device ID.
			subscribeToTopic(initTopic);

			log("Connection established to " + brokerHostName + " on topic " + Arrays.toString(initTopic));

			// Save start time
			mStartTime = System.currentTimeMillis();
			// Star the keep-alives
			startKeepAlives();
		}
		
		// Disconnect
		public void disconnect() {
			try {			
				stopKeepAlives();
				mqttClient.disconnect();
			} catch (MqttException e) {
				log("MqttException" + (e.getMessage() != null ? e.getMessage() : " NULL"), e);
			}
		}

		/*
		 * Send a request to the message broker to be sent messages published with
		 *  the specified topic name. Wildcards are allowed.
		 */
		private void subscribeToTopic(String[] topicName) throws MqttException {

			if ((mqttClient == null) || (!mqttClient.isConnected())) {
				// quick sanity check - don't try and subscribe if we don't have
				//  a connection
				log("Connection error" + "No connection");
			} else {
				int[] qosArray = new int[topicName.length];

				for (int i = 0; i < topicName.length; i++) {
					qosArray[i] = MQTT_QUALITY_OF_SERVICE;
				}
				mqttClient.subscribe(topicName, qosArray);
			}
		}
		/*
		 * Sends a message to the message broker, requesting that it be published
		 *  to the specified topic.
		 */
		private void publishToTopic(String topicName, String message) throws MqttException {		
			if ((mqttClient == null) || (!mqttClient.isConnected())) {
				// quick sanity check - don't try and publish if we don't have
				//  a connection				
				log("No connection to public to");		
			} else {
				mqttClient.publish(topicName, message.getBytes(),
						MQTT_QUALITY_OF_SERVICE, MQTT_RETAINED_PUBLISH);
			}
		}
		
		public void sendKeepAlive() throws MqttException {
			log("Sending keep alive");
			// publish to a keep-alive topic
			publishToTopic(MQTT_CLIENT_ID, mPrefs.getString(PREF_DEVICE_ID, ""));
		}

		/*
		 * Called if the application loses it's connection to the message broker.
		 */
		@Override
		public void connectionLost(Throwable throwable) {
			log("Loss of connection" + "connection downed");
			stopKeepAlives();
			// null itself
			mConnection = null;
			if (isNetworkAvailable()) {
				reconnectIfNecessary();
			}
		}

		/*
		 * Called when we receive a message from the message broker.
		 */
		@Override
		public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {
			String payLoad = new String(mqttMessage.getPayload());
//			showNotification(s);
			log("Got message: " + s);

			if (mMqttCallback != null) {
				mMqttCallback.getMessage(payLoad);
			}
		}

		@Override
		public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {

		}
	}
}